﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Collections.Specialized;
using System.Web.DynamicData;
using System.Web.Caching;
using System.ComponentModel;
using System.IO;
using System.Xml.Linq;
using System.Web.Security;
using System.Threading;
using System.Web.Util;
using System.Collections;
using System.Configuration.Provider;

namespace NotAClue.Web
{
    //http://msdn.microsoft.com/en-us/library/Aa479033
    //http://forums.asp.net/p/1022105/1384993.aspx
    //http://msdn.microsoft.com/en-us/magazine/cc163657.aspx
    public class XmlSiteMapProvider : SiteMapProvider
    {
        #region Constants
        private const String PROVIDER_NAME = "SecureMetadataSiteMapProvider";
        private const string CACHE_DEPENDENCY_NAME = "SecureMetaDataSiteMapCacheDependency";
        private const String DESCRIPTION = "description";
        private const String SITE_MAP_FILE = "SiteMapFile";
        private const String DEFAULT_SITE_MAP_FILE = "Web.sitemap";
        private const string ANONYMOUS = "anonymous";
        private const String ALLOW_ANONYMOUS_ACCESS = "AllowAnonymousAccess";
        #endregion

        #region Fields
        private readonly object _lock = new object();
        private Dictionary<String, SiteMapNode> _rootNode = new Dictionary<String, SiteMapNode>();
        private Dictionary<String, Hashtable> _urlTable = new Dictionary<String, Hashtable>();
        private Dictionary<String, Hashtable> _keyTable = new Dictionary<String, Hashtable>();
        private Dictionary<String, Hashtable> _childNodeCollectionTable = new Dictionary<string, Hashtable>();
        private Dictionary<String, Hashtable> _parentNodeTable = new Dictionary<string, Hashtable>();
        private String _siteMapFile;
        private String _username;
        private Boolean _allowAnonymousAccess = false;
        private MetaModel _model;
        #endregion

        #region Properties
        public MetaModel Model
        {
            get
            {
                if (_model == null)
                    _model = MetaModel.Default;
                return this._model;
            }
            set { this._model = value; }
        }

        private Boolean IsValidUser
        {
            get
            {
                if (_allowAnonymousAccess)
                {
                    _username = ANONYMOUS;
                    return true;
                }
                else if (HttpContext.Current.User.Identity.IsAuthenticated)
                {
                    _username = HttpContext.Current.User.Identity.Name;
                    return true;
                }

                return false;
            }
        }
        #endregion

        #region Initializartion
        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            // get provider name
            if (String.IsNullOrEmpty(name))
                name = "XmlSiteMapProvider";

            // get custom mappings
            if (!string.IsNullOrEmpty(config[SITE_MAP_FILE]))
                _siteMapFile = config[SITE_MAP_FILE];
            else
                _siteMapFile = DEFAULT_SITE_MAP_FILE;

            // get allow anonymous access
            if (!string.IsNullOrEmpty(config[ALLOW_ANONYMOUS_ACCESS]))
                _allowAnonymousAccess = Convert.ToBoolean(config[ALLOW_ANONYMOUS_ACCESS]);

            // get description
            if (string.IsNullOrEmpty(config[DESCRIPTION]))
            {
                config.Remove(DESCRIPTION);
                config.Add(DESCRIPTION, "MetaData Site Map Provider for Dynamic Data");
            }

            base.Initialize(name, config);
        }
        #endregion

        #region implemented methods
        public SiteMapNode BuildSiteMap()
        {
            if (!IsValidUser)
                return null;

            // need to make sure we start with a clean slate
            if (_rootNode.Keys.Contains(_username) && _rootNode[_username] != null)
            {
                return _rootNode[_username];
            }

            lock (_lock)
            {
                // setup user storage
                _rootNode.Add(_username, new SiteMapNode(this, "RootXmlSiteMapProvider"));
                _parentNodeTable.Add(_username, new Hashtable());
                _childNodeCollectionTable.Add(_username, new Hashtable());
                _urlTable.Add(_username, new Hashtable());
                _keyTable.Add(_username, new Hashtable());

                // get content from site map file.
                var sitemapFile = HttpContext.Current.Server.MapPath("~/" + _siteMapFile);

                // check for existence of site map file
                if (File.Exists(sitemapFile))
                {
                    // if site map file exists then add node from it

                    // load site map file
                    var sitemap = XDocument.Load(sitemapFile);
                    var root = sitemap.Descendants().Nodes().First() as XElement;

                    foreach (XElement element in root.Nodes().OfType<XElement>())
                    {
                        // add site map file element
                        SetXmlChildren(element, _rootNode[_username]);
                    }
                }
            }

            // not sure if this is needed no real explanation
            // was given in the samples I've seen.
            HttpRuntime.Cache.Insert(CACHE_DEPENDENCY_NAME, new object());

            return _rootNode[_username];
        }

        private void SetXmlChildren(XElement element, SiteMapNode parentNode)
        {
            SiteMapNode node = null;
            String[] usersRoles = null;

            if (_username != ANONYMOUS)
                usersRoles = Roles.GetRolesForUser(HttpContext.Current.User.Identity.Name);

            var url = element.Attributes().FirstOrDefault(a => a.Name == "url");
            var key = element.Attributes().FirstOrDefault(a => a.Name == "title");

            if (url != null || key != null)
            {
                if (url != null)
                    node = new SiteMapNode(this, url.Value, url.Value);
                else if (key != null)
                    node = new SiteMapNode(this, key.Value, String.Empty);

                // add attribute from element
                foreach (var attribute in element.Attributes())
                {
                    switch (attribute.Name.ToString())
                    {
                        //case "description":
                        //    node.Description = attribute.Value;
                        //    break;
                        case "resourceKey":
                            // used for localization
                            node.ResourceKey = attribute.Value;
                            break;
                        case "roles":
                            // used for security trimming
                            //node.Roles = attribute.Value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                            node.Roles = attribute.Value.ToArray();
                            break;
                        case "title":
                            node.Title = attribute.Value;
                            break;
                        // don't bother with these as there is no property to match.
                        case "siteMapFile":
                        case "securityTrimmingEnabled":
                        case "provider":
                        case "url":
                        default:
                            break;
                    }
                }

                if (node.Roles.Contains("*") || (usersRoles != null && usersRoles.HasAnyRole(node.Roles)))
                    AddNode(node, parentNode, true);

                if (element.HasElements)
                {
                    // get child elements
                    foreach (var childElement in element.Descendants())
                    {
                        // add child elements recursively 
                        SetXmlChildren(childElement, node);
                    }
                }
            }
            else
            {
                var provider = element.Attributes().FirstOrDefault(a => a.Name == "provider");
                node = GetNodeFromProvider(provider.Value);

                foreach (SiteMapNode n in node.ChildNodes)
                {
                    if (n.Roles.Contains("*") || (usersRoles != null && usersRoles.HasAnyRole(n.Roles)))
                        AddNodeWithChildren(n, parentNode);
                }
            }
        }

        protected void Clear()
        {
            if (!IsValidUser)
                return;

            try
            {
                Monitor.Enter(_lock);
                if (this._childNodeCollectionTable != null)
                    this._childNodeCollectionTable.Clear();

                if (this._urlTable != null)
                    this._urlTable.Clear();

                if (this._parentNodeTable != null)
                    this._parentNodeTable.Clear();

                if (this._keyTable != null)
                    this._keyTable.Clear();
            }
            finally
            {
                Monitor.Exit(_lock);
            }
        }

        protected void AddNodeWithChildren(SiteMapNode node, SiteMapNode parentNode)
        {
            AddNode(node, parentNode, false);
            // now add any children
            foreach (SiteMapNode n in node.ChildNodes)
            {
                AddNodeWithChildren(n, node);
            }
        }

        protected override void AddNode(SiteMapNode node, SiteMapNode parentNode)
        {
            AddNode(node, parentNode, false);
        }

        protected void AddNode(SiteMapNode node, SiteMapNode parentNode, Boolean staticNode)
        {
            if (node == null)
                throw new ArgumentNullException("node");

            try
            {
                Monitor.Enter(_lock);

                bool flag = false;
                string url = node.Url;
                if (!string.IsNullOrEmpty(url))
                {
                    //System.Web.Util
                    if (VirtualPathUtility.IsAbsolute(url))
                        url = VirtualPathUtility.ToAppRelative(url);

                    var cleanedUrl = url.TruncateAtLast('/');
                    if (!staticNode)
                        url = cleanedUrl;

                    //var urlParts = url.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
                    //if (!staticNode && urlParts.Length > 1)
                    //    url = urlParts[urlParts.Length - 1].Replace(" ", "");

                    if (UrlTable != null && UrlTable[url] != null)
                        throw new InvalidOperationException("Multiple Nodes With Identical URL");

                    flag = true;
                }

                string key = node.Key;
                if (KeyTable.Contains(key))
                    throw new InvalidOperationException("Multiple Nodes With Identical Key");

                KeyTable[key] = node;
                if (flag)
                    UrlTable[url] = node;

                if (parentNode != null)
                {
                    ParentNodeTable[node] = parentNode;

                    if (ChildNodeCollectionTable[parentNode] == null)
                        ChildNodeCollectionTable[parentNode] = new SiteMapNodeCollection();

                    ((SiteMapNodeCollection)ChildNodeCollectionTable[parentNode]).Add(node);
                }
            }
            finally
            {
                Monitor.Exit(_lock);
            }
        }

        public override SiteMapNode FindSiteMapNode(string rawUrl)
        {
            if (!IsValidUser)
                return null;

            if (rawUrl == null)
                throw new ArgumentNullException("rawUrl");

            rawUrl = rawUrl.Trim();
            if (rawUrl.Length == 0)
                return null;

            if (VirtualPathUtility.IsAbsolute(rawUrl))
                rawUrl = VirtualPathUtility.ToAppRelative(rawUrl);

            String url = rawUrl.TruncateAtLast('/');

            //var splitUrl = rawUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
            //if (splitUrl.Length > 1)
            //    url = splitUrl[1];

            Object node = this.UrlTable[url];

            if (node == null)
                node = this.UrlTable[rawUrl];

            this.BuildSiteMap();

            return ReturnNodeIfAccessible((SiteMapNode)node);
        }

        public override SiteMapNode FindSiteMapNodeFromKey(string key)
        {
            if (!IsValidUser)
                return null;

            SiteMapNode node = base.FindSiteMapNodeFromKey(key);
            if (node == null)
                node = (SiteMapNode)this.KeyTable[key];

            return ReturnNodeIfAccessible(node);
        }

        public override SiteMapNodeCollection GetChildNodes(SiteMapNode node)
        {
            if (!IsValidUser)
                return null;
            //if (!HttpContext.Current.User.Identity.IsAuthenticated)
            //    return null;

            if (node == null)
                throw new ArgumentNullException("node");

            this.BuildSiteMap();

            SiteMapNodeCollection collection = (SiteMapNodeCollection)this.ChildNodeCollectionTable[node];
            if (collection == null)
            {
                SiteMapNode node2 = (SiteMapNode)this.KeyTable[node.Key];
                if (node2 != null)
                    collection = (SiteMapNodeCollection)this.ChildNodeCollectionTable[node2];
            }
            if (collection == null)
                return new SiteMapNodeCollection();

            if (!base.SecurityTrimmingEnabled)
                return SiteMapNodeCollection.ReadOnly(collection);

            HttpContext current = HttpContext.Current;
            SiteMapNodeCollection nodes2 = new SiteMapNodeCollection(collection.Count);
            foreach (SiteMapNode node3 in collection)
            {
                if (node3.IsAccessibleToUser(current))
                    nodes2.Add(node3);
            }
            return SiteMapNodeCollection.ReadOnly(nodes2);
        }

        public override SiteMapNode GetParentNode(SiteMapNode node)
        {
            if (!IsValidUser)
                return null;
            //if (!HttpContext.Current.User.Identity.IsAuthenticated)
            //    return null;

            if (node == null)
                throw new ArgumentNullException("node");

            this.BuildSiteMap();

            SiteMapNode parentNode = (SiteMapNode)this.ParentNodeTable[node];
            if (parentNode == null)
            {
                SiteMapNode node3 = (SiteMapNode)this.KeyTable[node.Key];
                if (node3 != null)
                    parentNode = (SiteMapNode)this.ParentNodeTable[node3];
            }

            if ((parentNode == null) && (this.ParentProvider != null))
                parentNode = this.ParentProvider.GetParentNode(node);

            return ReturnNodeIfAccessible(parentNode);
        }

        protected override void RemoveNode(SiteMapNode node)
        {
            if (node == null)
                throw new ArgumentNullException("node");

            try
            {
                Monitor.Enter(_lock);
                SiteMapNode node2 = (SiteMapNode)this.ParentNodeTable[node];
                if (this.ParentNodeTable.Contains(node))
                    this.ParentNodeTable.Remove(node);

                if (node2 != null)
                {
                    SiteMapNodeCollection nodes = (SiteMapNodeCollection)this.ChildNodeCollectionTable[node2];
                    if ((nodes != null) && nodes.Contains(node))
                        nodes.Remove(node);
                }
                string url = node.Url;
                if (((url != null) && (url.Length > 0)) && this.UrlTable.Contains(url))
                    this.UrlTable.Remove(url);

                string key = node.Key;
                if (this.KeyTable.Contains(key))
                    this.KeyTable.Remove(key);
            }
            finally
            {
                Monitor.Exit(_lock);
            }
        }

        protected override SiteMapNode GetRootNodeCore()
        {
            if (!IsValidUser)
                return null;

            BuildSiteMap();
            return _rootNode[_username];
        }

        internal SiteMapNode ReturnNodeIfAccessible(SiteMapNode node)
        {
            if ((node != null) && node.IsAccessibleToUser(HttpContext.Current))
                return node;

            return null;
        }

        internal IDictionary ChildNodeCollectionTable
        {
            get
            {
                //if (!HttpContext.Current.User.Identity.IsAuthenticated)
                //    return null;

                if (this._childNodeCollectionTable[_username] == null)
                {
                    try
                    {
                        Monitor.Enter(_lock);
                        if (this._childNodeCollectionTable[_username] == null)
                            this._childNodeCollectionTable[_username] = new Hashtable();
                    }
                    finally
                    {
                        Monitor.Exit(_lock);
                    }
                }
                return this._childNodeCollectionTable[_username];
            }
        }

        internal IDictionary KeyTable
        {
            get
            {
                //if (!HttpContext.Current.User.Identity.IsAuthenticated)
                //    return null;

                if (this._keyTable[_username] == null)
                {
                    try
                    {
                        Monitor.Enter(_lock);
                        if (this._keyTable[_username] == null)
                            this._keyTable[_username] = new Hashtable();
                    }
                    finally
                    {
                        Monitor.Exit(_lock);
                    }
                }
                return this._keyTable[_username];
            }
        }

        internal IDictionary ParentNodeTable
        {
            get
            {
                //if (!HttpContext.Current.User.Identity.IsAuthenticated)
                //    return null;

                if (this._parentNodeTable == null)
                {
                    try
                    {
                        Monitor.Enter(_lock);
                        if (this._parentNodeTable[_username] == null)
                            this._parentNodeTable[_username] = new Hashtable();
                    }
                    finally
                    {
                        Monitor.Exit(_lock);
                    }
                }
                return this._parentNodeTable[_username];
            }
        }

        internal IDictionary UrlTable
        {
            get
            {
                //if (!HttpContext.Current.User.Identity.IsAuthenticated)
                //    return null;

                if (this._urlTable[_username] == null)
                {
                    try
                    {
                        Monitor.Enter(_lock);
                        if (this._urlTable[_username] == null)
                            this._urlTable[_username] = new Hashtable(StringComparer.OrdinalIgnoreCase);
                    }
                    finally
                    {
                        Monitor.Exit(_lock);
                    }
                }
                return this._urlTable[_username];
            }
        }
        #endregion

        private SiteMapNode GetNodeFromProvider(string providerName)
        {
            SiteMapProvider provider = GetProviderFromName(providerName);
            //provider.BuildSiteMap();
            SiteMapNode node = provider.RootNode;
            return node;
        }

        private SiteMapProvider GetProviderFromName(string providerName)
        {
            SiteMapProvider provider = SiteMap.Providers[providerName];
            if (provider == null)
                throw new ProviderException(String.Format("Provider '{0}' not found.", providerName));

            return provider;
        }

        //public override bool IsAccessibleToUser(HttpContext context, SiteMapNode node)
        //{
        //    if (node == null)
        //    {
        //        throw new ArgumentNullException("node");
        //    }
        //    if (context == null)
        //    {
        //        throw new ArgumentNullException("context");
        //    }
        //    if (!this.SecurityTrimmingEnabled)
        //    {
        //        return true;
        //    }
        //    if (node.Roles != null)
        //    {
        //        foreach (string str in node.Roles)
        //        {
        //            if ((str == "*") || ((context.User != null) && context.User.IsInRole(str)))
        //            {
        //                return true;
        //            }
        //        }
        //    }
        //    System.Web.VirtualPath virtualPath = node.VirtualPath;
        //    return (((virtualPath != null) && virtualPath.IsWithinAppRoot) && Util.IsUserAllowedToPath(context, virtualPath));
        //}
    }
}